/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.skatiger.jrove.spring.form.renderer;

import java.beans.PropertyEditor;
import java.io.IOException;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;

import org.skatiger.jrove.ResponseWriter;
import org.skatiger.jrove.ViewContext;
import org.skatiger.jrove.component.JroveAttr;
import org.skatiger.jrove.component.NamingContainer;
import org.skatiger.jrove.component.UIComponent;
import org.skatiger.jrove.renderer.Html;
import org.skatiger.jrove.renderer.RendererUtils;
import org.skatiger.jrove.spring.utils.ValueFormatter;
import org.springframework.beans.BeanWrapper;
import org.springframework.beans.PropertyAccessorFactory;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.support.BindStatus;

public class InputMultiCheckedRenderer extends AbstractFormElementRenderer {
    private static final String LABEL_FACET_NAME = "label";

    protected static final String ITEMS_PROPERTY_NAME = "items";
    protected static final String ITEM_VALUE_PROPERTY_NAME = "itemValue";

    @Override
    @SuppressWarnings("rawtypes")
    public void encodeBegin(ViewContext context, UIComponent component)
            throws IOException {
        Object itemsObject = component.getAttributes().get(ITEMS_PROPERTY_NAME);

        String itemValue = (String) component.getAttributes().get(
                ITEM_VALUE_PROPERTY_NAME);
        String valueProperty = (itemValue != null ? ObjectUtils
                .getDisplayString(itemValue) : null);

        Class<?> boundType = getBindStatus(component).getValueType();
        itemsObject = decideItemsObject(itemsObject, boundType);

        if (itemsObject.getClass().isArray()) {
            Object[] itemsArray = (Object[]) itemsObject;
            for (int i = 0; i < itemsArray.length; i++) {
                Object item = itemsArray[i];
                writeObjectEntry(context, component, valueProperty, item, i);
            }
        } else if (itemsObject instanceof Collection) {
            final Collection optionCollection = (Collection) itemsObject;
            int itemIndex = 0;
            for (Iterator it = optionCollection.iterator(); it.hasNext(); itemIndex++) {
                Object item = it.next();
                writeObjectEntry(context, component, valueProperty, item,
                        itemIndex);
            }
        } else if (itemsObject instanceof Map) {
            final Map optionMap = (Map) itemsObject;
            int itemIndex = 0;
            for (Iterator it = optionMap.entrySet().iterator(); it.hasNext(); itemIndex++) {
                Map.Entry entry = (Map.Entry) it.next();
                writeMapEntry(context, component, valueProperty, entry,
                        itemIndex);
            }
        } else {
            throw new IllegalArgumentException(
                    "Attribute 'items' must be an array, a Collection or a Map");
        }
    }

    /**
     * 决定bound Type
     * @param itemsObject
     * @param boundType
     * @return
     */
    private Object decideItemsObject(Object itemsObject, Class boundType) {
        Object _itemsObject = itemsObject;
        if (itemsObject == null && boundType != null && boundType.isEnum()) {
            _itemsObject = boundType.getEnumConstants();
        }
        if (_itemsObject == null && boundType != null && boundType.isArray()) {//is boundType is array
            if (boundType.getComponentType().isEnum()) {//boundType.getComponentType is enum
                _itemsObject = boundType.getComponentType().getEnumConstants();
            }
        }
        if (_itemsObject == null) {
            throw new IllegalArgumentException(
                    "Attribute 'items' is required and must be a Collection, an Array, a Map, Class type is enum or enum Array");
        }
        return _itemsObject;
    }

    public void encodeChildren(ViewContext context, UIComponent component)
            throws IOException {
        //do nothing
    }

    private void writeMapEntry(ViewContext context, UIComponent component,
            String valueProperty, Map.Entry entry, int itemIndex)
            throws IOException {

        Object mapKey = entry.getKey();
        BeanWrapper mapKeyWrapper = PropertyAccessorFactory
                .forBeanPropertyAccess(mapKey);
        Object renderValue = (valueProperty != null ? mapKeyWrapper
                .getPropertyValue(valueProperty) : mapKey.toString());
        writeElement(context, component, mapKey, renderValue, itemIndex);
    }

    private void writeObjectEntry(ViewContext context, UIComponent component,
            String valueProperty, Object item, int itemIndex)
            throws IOException {

        BeanWrapper wrapper = PropertyAccessorFactory
                .forBeanPropertyAccess(item);
        Object renderValue;
        if (valueProperty != null) {
            renderValue = wrapper.getPropertyValue(valueProperty);
        } else if (item instanceof Enum) {
            renderValue = ((Enum<?>) item).name();
        } else {
            renderValue = item;
        }
        writeElement(context, component, item, renderValue, itemIndex);
    }

    /**
     * 渲染CheckedItem前触发
     * @param context
     * @param component
     * @param item
     * @param value
     * @param itemIndex
     */
    protected void beforeRenderCheckedItem(ViewContext context,
            UIComponent component, MultiCheckedItem _item) {
    }

    /**
     * 渲染CheckedItem后触发
     * @param context
     * @param component
     * @param item
     * @param value
     * @param itemIndex
     */
    protected void afterRenderCheckedItem(ViewContext context,
            UIComponent component, MultiCheckedItem _item) {
    }

    private void writeElement(ViewContext context, UIComponent component,
            Object item, Object value, int itemIndex) throws IOException {
        ResponseWriter writer = context.getResponseWriter();
        MultiCheckedItem _item = new MultiCheckedItem(component, item, value,
                itemIndex);
        beforeRenderCheckedItem(context, component, _item);
        String element = (String) component.getAttributes().get("element");
        {//wrap element
            writer.startElement(element, component);
        }

        writer.startElement(Html.INPUT_ELEM, component);
        RendererUtils.renderHtmlAttributeValue(writer, Html.ID_ATTR,
                Html.ID_ATTR, _item.getId());// id
        RendererUtils.renderHtmlAttributeValue(writer, Html.NAME_ATTR,
                Html.NAME_ATTR, _item.getName());

        if (_item.isSelected()) {// is checked
            writer.writeAttribute(Html.CHECKED_ATTR, true, Html.CHECKED_ATTR);
        }
        RendererUtils.renderHtmlAttributeValue(writer, Html.VALUE_ATTR,
                Html.VALUE_ATTR, _item.getValue());//renderer form input value attribute

        RendererUtils.renderHtmlStandardAttributes(writer, component,
                Html.INPUT_ELEM_OPTIONAL_ATTRIBUTES_WITHOUT_NAME);

        writer.endElement(Html.INPUT_ELEM);
        {//renderer facet label 
            UIComponent labelFacet = component.getFacet(LABEL_FACET_NAME);// get label fact
            if (labelFacet != null) {
                String var = (String) component.getAttributes().get("var");
                Object previousValue = context.getAttribute(var);
                context.setAttribute(var, _item);// set attribute

                labelFacet.encodeAll(context);

                context.removeAttribute(var); // remove attribute
                if (previousValue != null) {
                    context.setAttribute(var, previousValue);
                }
            } else {
                writer.writeText(value, null);
            }
        }
        {//end wrap element
            writer.endElement(element);
        }
        afterRenderCheckedItem(context, component, _item);
    }

    public class MultiCheckedItem {
        private UIComponent component;
        private Object key;
        private Object value;
        private int itemIndex;

        public boolean isSelected() {
            return isOptionSelected(getBindStatus(), key, value);
        }

        public Object getKey() {
            return key;
        }

        public String getStyleClass() {
            return (String) component.getAttributes().get(
                    JroveAttr.STYLE_CLASS_ATTR);
        }

        public BindStatus getBindStatus() {

            return InputMultiCheckedRenderer.this.getBindStatus(component);
        }

        public String getValue() {
            BindStatus bindStatus = getBindStatus();
            PropertyEditor editor = (value != null ? bindStatus
                    .findEditor(value.getClass()) : null);
            String _value = ValueFormatter.getDisplayString(value, editor,
                    isHtmlEscape(component));
            return _value;
        }

        public String getType() {
            return (String) component.getAttributes().get(Html.TYPE_ATTR);
        }

        public String getName() {
            return (String) component.getAttributes().get(Html.NAME_ATTR);
        }

        public String getId() {
            String id = component.getId() + NamingContainer.SEPARATOR_CHAR
                    + itemIndex;
            return id;
        }

        /**
         * @param checked
         * @param item
         * @param value
         * @param itemIndex
         */
        public MultiCheckedItem(UIComponent component, Object key,
                Object value, int itemIndex) {
            this.component = component;
            this.key = key;
            this.value = value;
            this.itemIndex = itemIndex;
        }

    }

    /**
     * Determines whether the supplied value matched the selected value through
     * delegating to {@link SelectedValueComparator#isSelected}.
     */
    private boolean isOptionSelected(BindStatus bindStatus, Object item,
            Object value) {
        //first
        if (SelectedValueComparator.isSelected(bindStatus, value)) {
            return true;
        }
        //second
        if (value != item
                && SelectedValueComparator.isSelected(bindStatus, item)) {
            return true;
        }
        return false;
    }

}
